import java.net.*;
import java.io.*;
import java.lang.System;

/*
 * UDPClient sends requests to server
 * and gets a reply, then update the cache
 */
public class UDPClient {
    private String hostName;
	private int semanticOption;
    private int serverPort = 2222;
    private byte[] buffer = new byte[30000]; 
	private int mID = 0;
	private int t;	
	private DatagramPacket reply = null;
	private DatagramSocket aSocket = null;
	private Data data = null;
	private Data replyData = null;
	private Cache cache = new Cache();
	//To check if error occured when doing operation in the server
	private boolean errorOccurred = false; 
	//Keep error messages happening to the communication
	private String errorMsg = ""; 
    
    public UDPClient(int semanticOption,int t) {
        //hostName = getLocalHost();	
		hostName = "sc090v";
		this.semanticOption = semanticOption;		
		this.t = t;
    }
	
	private String getLocalHost() {
		try {
			InetAddress addr = InetAddress.getLocalHost();
			//byte[] ipAddr = addr.getAddress();
			return addr.getHostName();
		} catch(UnknownHostException e) {
			return "";
		}
	}
    
   /**
	* @return	true if the server replies, otherwise false
	*/
    private boolean sendData(byte[] message) {
		reply = null;
		replyData = null;
		errorOccurred = false;
		errorMsg = "";
		
        try {
            aSocket = new DatagramSocket();//use free local 
			//translate user-specified hostname to Internet address
            InetAddress aHost = InetAddress.getByName(hostName);
			//SocketAddress sockaddr = new InetSocketAddress(aHost, serverPort);
			//aSocket.bind(sockaddr);
			DatagramPacket request = new DatagramPacket(message,message.length,aHost,serverPort);
			aSocket.send(request);//Send packet
			System.out.println("DEBUG: Send Request to Server: " + new String(request.getData()));
			
			//Get reply after sending
			aSocket.setSoTimeout(10000);
			reply = new DatagramPacket(buffer, buffer.length);
			aSocket.receive(reply);
			
			System.out.println("DEBUG: Reply from Server: " + new String(reply.getData()));
			
			//Unmarshall & assemble data
			replyData = new Data(new String(reply.getData()));
			//Check if server replies an error message
			if(replyData.getOperation() == replyData.ERROR_OP) {
				errorOccurred = true;
			} else {
				/*Check if reply contains new contents to put in cache
				  by checking what is the request sent and reply received*/
				if(data.getOperation() == data.READ_OP 
					|| data.getOperation() == data.WRITE_OP 
					|| data.getOperation() == data.APPEND_OP
					|| replyData.getOperation() == data.UPDATE_OP) {
					//Update cache when necessary
					cache = new Cache(data.getFilename(),replyData.getTMserver(),replyData.getFileContents());
					//System.out.println("DEBUG: cache="+cache.getContents());
				}
			}
						
			buffer = new byte[30000]; //Clear the buffer 			
			data = null;
			return true;
        } catch(SocketTimeoutException e) {
			errorMsg = "Timeout has occurred.";
		} catch(SocketException e) {
			errorMsg = "A SocketException error occured.";
        } catch(UnknownHostException e) {
			errorMsg = "An UnknownHostException error occured.";
        } catch(IOException e) {
			errorMsg = "An IOException error occured.";
        } finally {
            if(aSocket != null) {
                aSocket.close();
            }
        }
		return false;
    }
	
	/**
	* Retransmit messages until it gets a reply
	* However, if there's no reply after 2 restransmits,
	* timeout error is shown to user
	*/
	private boolean sendRequest(String msg) {
		boolean reply = false;
		int count = 0;
		//Marshall data
        byte[] message = msg.getBytes();
		do{
			//System.out.println("DEBUG: sendRequest");
			//System.out.println("DEBUG: count="+count);
			if(sendData(message)) {
				reply = true;
			} else {
				//Timeout occurs
				if(count >= 2) {
					//Stop retransmitting after 2 tries
					break;
				} else {	
					count++;
				}
			}
		} while(!reply);
		return reply;
	}
    
   /**
	* Method to read contents from the file inclusive of the offset
	* @return	the contents of file requested or error message
	*/
    public String read(String filename,int offset,int numOfBytes) {
		//Check if cache is valid
        if(cacheValid(filename)) {
			System.out.println("DEBUG: Accessing Cache");
			return cache.getContents(offset,numOfBytes);
        } else {			
			//If cache not available, access server
			mID++;
			data = new Data(mID,data.REQUEST_MSG,data.READ_OP);
			data.setFilename(filename);
			if(sendRequest(data.getMessageStr())) {
				if(errorOccurred) {
					return replyData.getErrorMsg();
				} else {
					//Perform read operation from cache as cache has been updated
					return cache.getContents(offset,numOfBytes);
				}
			} else {
				return errorMsg;
			}
		}
    }
	
	private boolean cacheValid(String filename) {
		/*Check if cache contains the specified filename
		Check if cache needs to be evaluated
		if yes, get t_mserver from server*/
		if(cache.exists(filename) && cache.evaluateCache(t)) {
			return true;
		} else {
			mID++;
			data = new Data(mID,data.REQUEST_MSG,data.VALIDATE_OP);
			data.setFilename(filename);
			if(sendRequest(data.getMessageStr())) {
				if(cache.isCacheValid(replyData.getTMserver())) {
					return true;
				}			
			}
		}
		return false;
	}
    
    public String write(String filename,int offset,String content) {
        mID++;
		data = new Data(mID,data.REQUEST_MSG,data.WRITE_OP);
		data.setFilename(filename);
		data.setOffset(offset);
		data.setFileContents(content);
		if(sendRequest(data.getMessageStr())) {
			if(errorOccurred) {
				return replyData.getErrorMsg();
			} else {
				return "Writing has completed.";
			}
		} else {
			return errorMsg;
		}
    }
  
	public String monitor(String filename,int interval) {
		mID++;
		data = new Data(mID,data.REQUEST_MSG,data.MONITOR_OP);
		data.setFilename(filename);
		data.setInterval(interval);
		DatagramSocket bSocket = null;
		//boolean monitorEnd = false;
		if(sendRequest(data.getMessageStr())) {
			//Check if reply from server is an acknoledgement 
			if(errorOccurred) {
				return replyData.getErrorMsg();
			} else if(replyData.getOperation() == replyData.MONITOR_OP) {
				System.out.println("DEBUG: Acknowledgement from Server received");
				//Server sends acknowledgement
				//Starts waiting for reply from Server
				Data receivedData = null;
				try {
					bSocket = new DatagramSocket(serverPort);
					//bSocket.setReuseAddress(true);
					while(true) {
						System.out.println("DEBUG: Listening from Server for Times Up!");
						DatagramPacket request = new DatagramPacket(buffer, buffer.length);
						bSocket.receive(request);//blocked if no input
						System.out.println("DEBUG: Reply from Server: " + new String(request.getData()));
						//Unmarshall & assemble data
						receivedData = new Data(new String(request.getData()));	
						if(receivedData.getOperation() == receivedData.END_MONITOR_OP) {
							return "Monitoring of file " + filename + " has ended.";
						} else if(receivedData.getOperation() == receivedData.UPDATE_OP) {
							//update cache
							cache = new Cache(filename,receivedData.getTMserver(),receivedData.getFileContents());
						} else {
							return "Error!"; //Coding Error
						}
					}
				} catch(SocketException e) {
					return "A SocketException error occured.";
				} catch(UnknownHostException e) {
					return "An UnknownHostException error occured.";
				} catch(IOException e) {
					return "An IOException error occured.";
				} finally {
					if(bSocket != null) {
						bSocket.close();
					}
				}
			} 
		} else {
			return errorMsg;
		}
		return "";
	}
	
	public String createFile(String filename) {
		mID++;
		data = new Data(mID,data.REQUEST_MSG,data.CREATE_OP);
		data.setFilename(filename);
		if(sendRequest(data.getMessageStr())) {
			/*Check if server creates file successfully*/
			if(errorOccurred) {
				return replyData.getErrorMsg();
			} else {
				return "File " + filename + " created successfully.";
			}
		} else {
			return errorMsg;
		}
	}
	
	public String append(String filename,String content) {
		mID++;
		data = new Data(mID,data.REQUEST_MSG,data.APPEND_OP);
		data.setFilename(filename);
		data.setFileContents(content);
		if(sendRequest(data.getMessageStr())) {
			if(errorOccurred) {
				return replyData.getErrorMsg();
			} else {
				return "Writing has completed.";
			}
		} else {
			return errorMsg;
		}
	}
	
	public boolean closeServer() {
		mID++;
		data = new Data(mID,data.REQUEST_MSG,data.CLOSE_OP);
		return sendRequest(data.getMessageStr());
	}
}